﻿using Sustainalytics.Accounts.BusinessLogic;
using Sustainalytics.Admin.CRMSynchronization.Utility;
using Sustainalytics.Claims;
using Sustainalytics.DataLayer;
using Sustainalytics.DynamicsCRMGateway;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sustainalytics.Admin.CRMSynchronization
{
    public class AccountsSynchronization : IAccountsSync
    {
        private Guid _defaultProfile;
        private IDynamicsCrmGateway _crmGateway;
        private IAccountsRepository _accountsRepository;
        private ICompanyScreeningRepository _companyRepository;
        private IOperationClaimsRetriever _operationClaimsRetriever;
        private IAccountWithIpManager _accWithIpManager;

        private AccountsSynchronization()
        {
        }// prohibit public instantiation

        public static IAccountsSync GetInstance(IDynamicsCrmGateway crmGateway, IAccountsRepository accountsRepository,
            ICompanyScreeningRepository companyRepository, IOperationClaimsRetriever operationClaimsRetriever, IAccountWithIpManager accWithIpManager)
        {
            if (crmGateway == null)
            {
                throw new ArgumentException("An instance of type IDynamicsCrmGateway is needed in order to create an IAccountsSync instance!");
            }

            if (accountsRepository == null)
            {
                throw new ArgumentException("An instance of type IAccountsRepository is needed in order to create an IAccountsSync instance!");
            }

            if (companyRepository == null)
            {
                throw new ArgumentException("An instance of type ICompanyScreeningRepository is needed in order to create an IAccountsSync instance!");
            }

            if (operationClaimsRetriever == null)
            {
                throw new ArgumentException("An instance of type IOperationClaimsRetriever is needed in order to create an IAccountsSync instance!");
            }

            if (accWithIpManager == null)
            {
                throw new ArgumentException("An instance of type IAccountWithIpManager is needed in order to create an IAccountsSync instance!");
            }

            return new AccountsSynchronization
            {
                _crmGateway = crmGateway,
                _accountsRepository = accountsRepository,
                _companyRepository = companyRepository,
                _operationClaimsRetriever = operationClaimsRetriever,
                _accWithIpManager = accWithIpManager,
            };
        }

        public void SyncAccounts()
        {
            var currentSyncDate = DateTime.UtcNow;

            try
            {
                currentSyncDate += _crmGateway.GetCrmTimeOffset();// translate the local value into the CRM reference system

                CrmSyncEventSource.Log.AccountsSyncStart(currentSyncDate);

                var changedCrmAccounts = _crmGateway.ListChangedAccounts(_accountsRepository.GetLastSyncDate(SyncType.Account));

                var crmAccountsIds = _crmGateway.ListAccountsIds();

                var businessAccountsIds = _accountsRepository.ListAccountsIds();

                var deletedAccountsIds = businessAccountsIds.Except(crmAccountsIds).ToList();

                RemoveAccountsFromRedis(deletedAccountsIds);
                RemoveAccountsFromIPAuth(deletedAccountsIds);

                _accountsRepository.DeactivateAccounts(deletedAccountsIds, currentSyncDate);

                List<Account> upsertedCrmAccounts = changedCrmAccounts.Where(a => businessAccountsIds.Contains(a.Id) ||
                                                                        a.RelationshipType != RelationshipType.FormerClient)// exclude the newly created CRM accounts which come as former clients
                                                                .ToList();

                upsertedCrmAccounts.ForEach(a =>
                {
                    a.Status = a.RelationshipType == RelationshipType.FormerClient ? Status.Inactive : a.Status;
                    a.LastCrmSync = currentSyncDate;
                    if (!businessAccountsIds.Contains(a.Id))//new account
                    {
                        a.CurrentEsgProfile = GetDefaultProfile(currentSyncDate);
                    }
                    else //updated account
                    {
                        var acc = _accountsRepository.GetAccountById(a.Id);

                        if (acc.CurrentEsgProfile == Guid.Empty)
                            a.CurrentEsgProfile = GetDefaultProfile(currentSyncDate);
                        else
                            a.CurrentEsgProfile = acc.CurrentEsgProfile;

                        a.UpcomingEsgProfile = acc.UpcomingEsgProfile;
                        if (a.Name != acc.Name)
                        {
                            RenameAccountInRedis(acc, a);
                            RenameAccountInUsersColection(a);
                            RenameAccountInIPAuth(a);
                        }
                        if ((a.Status == Status.Inactive && a.Status != acc.Status)
                            || (!a.HasPlatformAccess && acc.HasPlatformAccess)) //account has became inactive
                        {
                            RemoveAccountsFromIPAuth(new List<Guid>() { acc.Id });
                        }

                        ResetIsLegalAgreementAcceptedWhenRelationshipTypeChanges(a, acc);
                    }
                });//apply deactivation business rule

                var activeAccounts = upsertedCrmAccounts.Where(a => a.Status == Status.Active).Select(a => a.Id).ToList();

                //this must execute before saveAccounts
                PersistReactivatedEvents(activeAccounts, currentSyncDate);

                _accountsRepository.SaveAccounts(upsertedCrmAccounts);

                _accountsRepository.SaveLastSync(new Entities.Synchronization
                {
                    Id = SyncType.Account,
                    LastSuccessfulSync = currentSyncDate
                });

                var deleted = deletedAccountsIds.Count;
                var created = upsertedCrmAccounts.Count(a => !businessAccountsIds.Contains(a.Id));
                var updated = upsertedCrmAccounts.Count - created;

                CrmSyncEventSource.Log.AccountsSyncSucceeded(currentSyncDate, created, deleted, updated);
            }
            catch (Exception e)
            {
                CrmSyncEventSource.Log.AccountsSyncFailed(currentSyncDate, e.GetType().ToString(), e.Message, ExceptionUtility.GetInfo(e));

                throw;
            }
            finally
            {
                CrmSyncEventSource.Log.AccountsSyncStop(currentSyncDate);
            }
        }

        private void ResetIsLegalAgreementAcceptedWhenRelationshipTypeChanges(Account crmAccount, Account account)
        {
            if (crmAccount.RelationshipType == account.RelationshipType)
                return;

            var users = _accountsRepository.GetUsersByAccountId(account.Id, true);
            users.ForEach(user => { user.IsLegalAgreementAccepted = false; });
            _accountsRepository.SaveUsers(users);
        }

        private void RemoveAccountsFromIPAuth(List<Guid> deletedAccountsIds)
        {
            deletedAccountsIds.ForEach(id => _accWithIpManager.DeleteAccount(id));
        }

        private void RenameAccountInIPAuth(Account a)
        {
            _accWithIpManager.RenameAccount(a);
        }

        private void RenameAccountInUsersColection(Account account)
        {
            _accountsRepository.UpdateAccountNameForUsers(account);
            _accountsRepository.UpdateAccountNameForFakeIPUsers(account);
        }

        private void RenameAccountInRedis(Account oldAcc, Account newAcc)
        {
            var claims = _operationClaimsRetriever.GetUserClaims(oldAcc);
            _operationClaimsRetriever.DeleteUser(oldAcc);
            var userClaim = new UserClaim() { Id = newAcc.Id, Account = newAcc.Name, Claims = claims.ToList() };
            _operationClaimsRetriever.AddUserClaims(new List<UserClaim>() { userClaim });
        }

        private void RemoveAccountsFromRedis(List<Guid> deletedAccountsIds)
        {
            deletedAccountsIds.ForEach(id =>
            {
                var account = _accountsRepository.GetAccountById(id);

                if (account.Status == Status.Active)//only active accounts need to be deleted from Redis (the rest are already deleted by a previous sync)
                    _operationClaimsRetriever.DeleteUser(account);
            });
        }

        private Guid GetDefaultProfile(DateTime currentSyncDate)
        {
            if (_defaultProfile == Guid.Empty)
            {
                var defProfile = _companyRepository.GetDefaultProfile();
                if (defProfile == null)
                {
                    CrmSyncEventSource.Log.NoDefaultProfileFound(currentSyncDate);
                }
                else
                {
                    _defaultProfile = defProfile.Id;
                }
            }
            return _defaultProfile;
        }

        private void PersistReactivatedEvents(List<Guid> activatedAccountIds, DateTime eventTime)
        {
            if (activatedAccountIds == null || activatedAccountIds.Count == 0)
                return;
            int take = 20;

            var reactivatedEvents =
                (from a in activatedAccountIds.Buffer(take)
                 select this._accountsRepository.ListAccountsByIdAndStatus(a.ToList(), Status.Inactive) ?? new List<Guid>(0))
                .SelectMany(a => a.Select(
                    id => new ReactivatedSyncEvent
                    {
                        Id = id,
                        EventDate = eventTime
                    })
                    );

            foreach (var reactivatedAccountsPage in reactivatedEvents.Buffer<ReactivatedSyncEvent>(take))
            {
                this._accountsRepository.SaveReactivatedSyncEvents(reactivatedAccountsPage.ToList());
            }
        }
    }
}
